using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using chromium.Cert;
using chromium.GAE;
using chromium.HTTP.Parser;
using chromium.HTTP.Runtime;
using chromium.Log;
using chromium.Utils;

namespace chromium.Proxy
{
    public sealed class HttpClient : IDisposable
    {
        private static readonly ILog logger = LogManager.GetLogger(typeof(HttpClient));
        private readonly string clientName;
        private bool disposed;
        private byte[] localBuffer = new byte[1024];
        private byte[] remoteBuffer;
        private Stream localStream;
        private SslStream remoteStream;
        private Socket localSocket;
        private Socket remoteSocket;
        private HttpRequestParser requestParser = new HttpRequestParser();

        private readonly AsyncCallback onLocalReceiveCompletedCallBack;
        private readonly AsyncCallback onLocalReadedCallBack;
        private readonly AsyncCallback onRemoteSendCompletedCallBack;
        private readonly AsyncCallback onRemoteReadedCallBack;
        private readonly AsyncCallback onRequestProcessedCallBack;
        private readonly WaitOrTimerCallback timoutCallback;
        private RegisteredWaitHandle waitHandle;

        public HttpClient(Socket localSocket)
        {
            onLocalReceiveCompletedCallBack = new AsyncCallback(OnLocalReceiveCompleted);
            onLocalReadedCallBack = new AsyncCallback(OnLocalReaded);
            onRemoteSendCompletedCallBack = new AsyncCallback(OnRemoteSendCompleted);
            onRemoteReadedCallBack = new AsyncCallback(OnRemoteReaded);
            onRequestProcessedCallBack = new AsyncCallback(ReceiveRequest);
            timoutCallback = new WaitOrTimerCallback(TimoutCallback);

            this.localSocket = localSocket;

            localStream = new NetworkStream(localSocket, true);

            clientName = localSocket.RemoteEndPoint.ToString();
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (disposed)
                return;
            disposed = true;
            localSocket.Dispose();
            localStream.Dispose();

            if (remoteSocket != null)
            {
                remoteSocket.Close();
                remoteSocket.Dispose();
            }
            if (remoteStream != null)
            {
                remoteStream.Close();
                remoteStream.Dispose();
            }
        }

        #endregion

        public void ReceiveRequest(IAsyncResult ar)
        {
            IAsyncResult asyncResult = localStream.BeginRead(localBuffer, 0, localBuffer.Length, onLocalReceiveCompletedCallBack, localStream);
            if (asyncResult.IsCompleted || asyncResult.CompletedSynchronously)
                return;
            waitHandle = ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, timoutCallback, localStream, 8000, true);
        }

        private void OnLocalReceiveCompleted(IAsyncResult ar)
        {
            try
            {
                if (waitHandle != null)
                    waitHandle.Unregister(ar.AsyncWaitHandle);

                int bytes = localStream.EndRead(ar);

                if (bytes == 0)
                {
                    logger.Debug(clientName + ": Local HTTP DisConnected");
                    Dispose();
                    return;
                }

                requestParser.ParseReuest(localBuffer, 0, bytes);

                if (requestParser.HasRequest())
                {
                    ProcessRequests();
                }
                else
                {
                    ReceiveRequest(null);
                }
            }
            catch (Exception oe)
            {
                logger.Error(clientName + ": Local HTTP Error", oe);
                Dispose();
            }
        }

        private void TimoutCallback(object state, bool timedout)
        {
            if (timedout)
            {
                if (localSocket.Connected)
                {
                    localSocket.Shutdown(SocketShutdown.Both);
                    logger.DebugFormat(clientName + ": Close connection due to inacitvity.");
                }
            }
        }

        private void ProcessRequests()
        {
            if (requestParser.HasRequest())
            {
                HttpRequest request = requestParser.GetRequest();

                if ("CONNECT".Equals(request.Method, StringComparison.OrdinalIgnoreCase))
                {
                    SslHandShake(request);

                }
                else
                {
                    ProxyRuntime.Execute(request, new HttpResponse(localStream), onRequestProcessedCallBack);
                }
            }
        }

        private void SslHandShake(HttpRequest request)
        {
            localStream.Write(Resources.Instance.SslHelloBytes, 0, Resources.Instance.SslHelloBytes.Length);

            localStream.Flush();

            localStream = SSPICertUtils.WrapStream(request.Headers["host"], localStream);

            if (Settings.EnableDirectConnect
                && Settings.DirectHost.Exists((host) => request.Host.IndexOf(host, StringComparison.OrdinalIgnoreCase) > -1))
            {
                remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                remoteSocket.BeginConnect(DnsHelper.GetHost(request.Host), 443, OnRemoteConnect, request);
                logger.DebugFormat("SSL Direct Connect: {0}", request.Host);
                return;
            }

            logger.DebugFormat("SSL Tunnel Connect: {0}", request.Host);

            ReceiveRequest(null);
        }

        #region forward https socket

        private void OnRemoteConnect(IAsyncResult ar)
        {
            try
            {
                HttpRequest request = (HttpRequest)ar.AsyncState;

                remoteSocket.EndConnect(ar);

                remoteStream = new SslStream(new NetworkStream(remoteSocket));

                remoteStream.BeginAuthenticateAsClient(request.Host, new X509CertificateCollection(), SslProtocols.Default, true, OnRemoteAuthenticate, remoteStream);
            }
            catch (Exception oe)
            {
                logger.Error("Remote SSL Error", oe);
                Dispose();
            }
        }

        private void OnRemoteAuthenticate(IAsyncResult ar)
        {
            try
            {
                remoteStream.EndAuthenticateAsClient(ar);

                if (!remoteStream.IsEncrypted
                    || !remoteStream.IsSigned)
                    throw new ApplicationException("SSL stream is not encrypted.");

                remoteBuffer = new byte[1024];

                localStream.BeginRead(localBuffer, 0, localBuffer.Length, onLocalReadedCallBack, localStream);
                remoteStream.BeginRead(remoteBuffer, 0, remoteBuffer.Length, onRemoteReadedCallBack, remoteStream);
            }
            catch (Exception oe)
            {
                logger.Error("Remote SSL Error", oe);
                Dispose();
            };
        }

        private void OnRemoteReaded(IAsyncResult ar)
        {
            try
            {
                int bytes = remoteStream.EndRead(ar);

                if (bytes == 0)
                {
                    logger.Debug("Remote SSL DisConnected");
                    if (localSocket.Connected)
                        localSocket.Shutdown(SocketShutdown.Both);
                    else
                        Dispose();
                    return;
                }

                localStream.Write(remoteBuffer, 0, bytes);

                remoteStream.BeginRead(remoteBuffer, 0, remoteBuffer.Length, onRemoteReadedCallBack, remoteStream);
            }
            catch (Exception oe)
            {
                logger.Error("Remote SSL Error", oe);
                Dispose();
            }
        }

        private void OnLocalReaded(IAsyncResult ar)
        {
            try
            {
                int bytes = localStream.EndRead(ar);

                if (bytes == 0)
                {
                    logger.Debug("Local SSL DisConnected");
                    if (remoteSocket.Connected)
                        remoteSocket.Shutdown(SocketShutdown.Both);
                    else
                        Dispose();
                    return;
                }

                remoteStream.BeginWrite(localBuffer, 0, bytes, onRemoteSendCompletedCallBack, remoteStream);
            }
            catch (Exception oe)
            {
                logger.Error("Local SSL Error", oe);
                Dispose();
            }
        }

        private void OnRemoteSendCompleted(IAsyncResult ar)
        {
            try
            {
                remoteStream.EndWrite(ar);

                localStream.BeginRead(localBuffer, 0, localBuffer.Length, onLocalReadedCallBack, localStream);
            }
            catch (Exception oe)
            {
                logger.Error("Remote SSL Error", oe);
                Dispose();
            }
        }

        #endregion
    }
}